package com.asmtest.visitor;

import com.alibaba.fastjson.JSON;
import com.util.AnnotationInfo;
import com.util.ClassInfo;
import com.util.MethodInfo;
import com.util.ParamInfo;
import com.util.description.MethodSignature;
import com.util.description.PrimitiveTypeDesc;
import com.util.description.TypeDesc;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.signature.SignatureReader;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static com.asmtest.visitor.TypeDescUtils.getClassName;
import static com.asmtest.visitor.TypeDescUtils.getGenericTypeString;


/**
 * <pre>{@code
 * <!-- asm -->
 * <dependency>
 *      <groupId>org.ow2.asm</groupId>
 *      <artifactId>asm</artifactId>
 *      <version>9.1</version>
 * </dependency>
 * }</pre>
 *
 * @author yutianhong
 * @version 1.0
 * @since 2023/11/28 10:28
 */
public class AsmScanner {

    public static void main(String[] args) throws IOException {
        String className = "com/asmtest/visitor/MyClass.class";

        try (InputStream inputStream = AsmScanner.class.getClassLoader().getResourceAsStream(className)) {
            if (inputStream == null) {
                System.out.println("无法找到类文件");
                return;
            }

            ClassReader classReader = new ClassReader(inputStream);
            Consumer<ClassInfo> consumer = classInfo -> System.out.println("classInfo = " + JSON.toJSONString(classInfo, true));
            classReader.accept(new MyDetectingClassVisitor(consumer), 0);
        }
    }

    static class MyDetectingClassVisitor extends ClassVisitor {
        private final ClassInfo classInfo;
        private final Consumer<ClassInfo> consumer;

        public MyDetectingClassVisitor(Consumer<ClassInfo> consumer) {
            super(Opcodes.ASM9);
            classInfo = new ClassInfo();
            classInfo.methodInfoList = new ArrayList<>();
            classInfo.annotationInfoList = new ArrayList<>();
            this.consumer = consumer;
        }

        @Override
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            boolean isInterface = isInterface(access);
            classInfo.className = name.replace("/", ".");
            classInfo.superClassName = superName;
            classInfo.interfaceList = Arrays.asList(interfaces);
        }

        @Override
        public void visitEnd() {
            if (consumer != null) {
                consumer.accept(classInfo);
            }
        }

        @Override
        public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
            Type type = Type.getType(descriptor);
            String className = type.getClassName();
            return new CustomAnnotationVisitor(values -> {
                AnnotationInfo annotationInfo = new AnnotationInfo();
                annotationInfo.values = values;
                annotationInfo.className = className;
                classInfo.annotationInfoList.add(annotationInfo);
            });
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            MethodInfo methodInfo = new MethodInfo();
            methodInfo.methodName = name;
            methodInfo.annotationInfoList = new ArrayList<>();

            // 签名信息
            SignatureReader signatureReader;
            if (signature == null) {
                signatureReader = new SignatureReader(descriptor);
            } else {
                signatureReader = new SignatureReader(signature);
            }
            CustomMethodSignatureVisitor sv = new CustomMethodSignatureVisitor();
            signatureReader.accept(sv);
            MethodSignature methodSignature = sv.getMethodSignature();

            methodInfo.exceptionList = methodSignature.getExceptions()
                    .stream().map(TypeDesc::className).collect(Collectors.toList());
            // 入参基本信息
            List<TypeDesc> paramTypes = methodSignature.getParamTypes();
            ArrayList<ParamInfo> inParam = new ArrayList<>(paramTypes.size());
            for (int i = 0; i < paramTypes.size(); i++) {
                TypeDesc type = paramTypes.get(i);
                ParamInfo paramInfo = getParamInfo(i, type);
                inParam.add(paramInfo);
            }
            methodInfo.inParam = inParam;

            // 出参基本信息
            TypeDesc returnType = methodSignature.getReturnType();
            methodInfo.outParam = getParamInfo(returnType);

            // 注解信息
            return new MethodVisitor(Opcodes.ASM9) {
                @Override
                public AnnotationVisitor visitParameterAnnotation(int parameter, String descriptor, boolean visible) {
                    ParamInfo paramInfo = methodInfo.inParam.get(parameter);
                    Type type = Type.getType(descriptor);
                    String className = type.getClassName();
                    return new CustomAnnotationVisitor(values -> {
                        AnnotationInfo annotationInfo = new AnnotationInfo();
                        annotationInfo.values = values;
                        annotationInfo.className = className;
                        paramInfo.annotationInfoList.add(annotationInfo);
                    });
                }

                @Override
                public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
                    Type type = Type.getType(descriptor);
                    String className = type.getClassName();
                    return new CustomAnnotationVisitor(map -> {
                        AnnotationInfo annotationInfo = new AnnotationInfo();
                        annotationInfo.values = map;
                        annotationInfo.className = className;
                        methodInfo.annotationInfoList.add(annotationInfo);
                    });

                }

                @Override
                public void visitEnd() {
                    classInfo.methodInfoList.add(methodInfo);
                }
            };
        }

        private static ParamInfo getParamInfo(TypeDesc type) {
            return getParamInfo(0, type);
        }

        private static ParamInfo getParamInfo(int i, TypeDesc type) {
            ParamInfo paramInfo = new ParamInfo();
            paramInfo.id = i;
            paramInfo.className = getClassName(type);
            paramInfo.primitive = type.isPrimitive();
            paramInfo.array = type.isArray();
            paramInfo.wildcard = type.isWildcard();
            paramInfo.typeVariable = type.isTypeVariable();
            paramInfo.parameterized = type.isParameterized();
            paramInfo.voidType = isVoid(type);
            if (type.isParameterized()) {
                paramInfo.genericInfoList = type.asParameterized().getTypeArguments()
                        .stream().map(MyDetectingClassVisitor::getParamInfo).collect(Collectors.toList());
            }
            if (type.isTypeVariable()) {
                paramInfo.upperBound = type.asTypeVariable().getUpperBounds()
                        .stream().map(MyDetectingClassVisitor::getParamInfo).collect(Collectors.toList());
            }
            if (type.isWildcard()) {
                paramInfo.upperBound = type.asWildcard().getUpperBounds()
                        .stream().map(MyDetectingClassVisitor::getParamInfo).collect(Collectors.toList());
                paramInfo.lowerBounds = type.asWildcard().getLowerBounds()
                        .stream().map(MyDetectingClassVisitor::getParamInfo).collect(Collectors.toList());
            }
            paramInfo.genericType = getGenericTypeString(type);
            paramInfo.annotationInfoList = new ArrayList<>();
            return paramInfo;
        }
    }

    // =========== utils ===========

    private static boolean sameClass(String descriptor, Class<?> clazz) {
        assert clazz != null;
        assert descriptor != null;
        Type type = Type.getType(descriptor);
        return Objects.equals(type.getClassName(), clazz.getName());
    }

    private static boolean isInterface(int access) {
        return (access & Opcodes.ACC_INTERFACE) != 0;
    }

    private static boolean isVoid(TypeDesc type) {
        return type == PrimitiveTypeDesc.VOID;
    }

}
